#![allow(non_snake_case)]
use std::{cell::RefCell, rc::Rc};

use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
use objc2_foundation::{CGSize, NSMutableAttributedString, NSObject, NSObjectProtocol, NSSet, NSString, NSUInteger};
use objc2_ui_kit::{UIEvent, UIKeyInput, UIResponder, UITextInput, UITextInputTraits, UITouch, UITouchPhase, UIView};

use objc2::{rc::Id, runtime::AnyObject, runtime::ProtocolObject};
use objc2_foundation::{
	CGPoint, CGRect, MainThreadMarker, NSArray, NSAttributedStringKey, NSComparisonResult, NSDictionary, NSInteger, NSRange,
};
use objc2_ui_kit::{
	NSWritingDirection, UIPress, UIPressesEvent, UITextInputDelegate, UITextInputStringTokenizer, UITextInputTokenizer,
	UITextLayoutDirection, UITextPosition, UITextRange, UITextSelectionRect, UITextStorageDirection,
};

use crate::MTKView::MTKView;

use super::{Vars, WinKeyboard, WinRef, WinTouch};

pub struct ViewVars {
	vars: Rc<Vars>,
	input_delegate: RefCell<Option<Id<ProtocolObject<dyn UITextInputDelegate>>>>,
	tokenizer: RefCell<Option<Id<UITextInputStringTokenizer>>>,
	maked: RefCell<Id<NSMutableAttributedString>>,
}
impl ViewVars {
	pub fn new(vars: Rc<Vars>) -> Self {
		Self {
			vars,
			input_delegate: Default::default(),
			tokenizer: Default::default(),
			maked: RefCell::new(NSMutableAttributedString::new()),
		}
	}
}

declare_class! {
	pub struct XLoopView;
	unsafe impl ClassType for XLoopView {
		#[inherits(UIView,UIResponder,NSObject)]
		type Super = MTKView;
		type Mutability = mutability::MainThreadOnly;
		const NAME: &'static str = "XLoopView";
	}
	impl DeclaredClass for XLoopView {
		type Ivars = ViewVars;
	}
	unsafe impl NSObjectProtocol for XLoopView {}
	unsafe impl UITextInputTraits for XLoopView {}
	unsafe impl UIKeyInput for XLoopView {
		#[method(hasText)]
		unsafe fn hasText(&self) -> bool {
			// log::info!("hasText");
			self.ivars().maked.borrow().length() > 0
			// false
		}
		#[method(insertText:)]
		unsafe fn insertText(&self, text: &NSString) {
			log::warn!("insertText:{text}");
			let string = text.to_string();
			self.composition(types::Ime::new(types::Action::End,Some(string.into())));
			// self.ivars().text_store.borrow_mut().appendAttributedString(&NSMutableAttributedString::from_nsstring(text));
		}
		#[method(deleteBackward)]
		unsafe fn deleteBackward(&self){
			log::warn!("deleteBackward");
			self.composition(types::Ime::new(types::Action::Back,None));
			// let length = self.ivars().text_store.borrow().length();
			// self.ivars().text_store.borrow_mut().deleteCharactersInRange(NSRange::new(length-1,1))
		}
	}
	unsafe impl UITextInput for XLoopView {
		// Handling text input
		#[method_id(inputDelegate)]
		unsafe fn inputDelegate(&self)-> Option<Id<ProtocolObject<dyn UITextInputDelegate>>>{
			log::info!("inputDelegate");
			if let Some(v) = self.ivars().input_delegate.borrow().as_ref(){
				Id::retain(Id::as_ptr(v) as *mut _)
			}else{
				None
			}
		}
		#[method(setInputDelegate:)]
		unsafe fn setInputDelegate(&self,v: Option<&ProtocolObject<dyn UITextInputDelegate>>){
			log::info!("setInputDelegate");
			if let Some(v) = v {
				let v:Option<Id<ProtocolObject<dyn UITextInputDelegate>>> =	Id::retain(v as *const _ as *mut _);
				*self.ivars().input_delegate.borrow_mut() = v;
			}
		}
		// Replacing and returning text
		#[method_id(textInRange:)]
		unsafe fn textInRange(&self, range: &UITextRange) -> Option<Id<NSString>>{
			let range = TextRange::from_ui(range).to_range();
			let ret = self.ivars().maked.borrow().mutableString().substringWithRange(range);
			// log::info!("textInRange {}-{} ==={ret}===",range.location,range.length);
			Some(ret)
		}
		#[method(replaceRange:withText:)]
		unsafe fn replaceRange_withText(&self, range: &UITextRange, string: &NSString){
			log::warn!("replaceRange_withText {string:?}");
			let range = TextRange::from_ui(range).to_range();
			self.ivars().maked.borrow_mut().replaceCharactersInRange_withString(range,string);
		}
		// Working with marked and selected text
		#[method_id(selectedTextRange)]
		unsafe fn selectedTextRange(&self) -> Option<Id<UITextRange>> {
			let length = self.ivars().maked.borrow().length();
			// log::info!("selectedTextRange {}-{}",length,length);
			Some(TextRange::new(length,length).to_ui())
		}
		#[method(setSelectedTextRange:)]
		unsafe fn setSelectedTextRange(&self, _: Option<&UITextRange>){
			log::info!("setSelectedTextRange");
		}
		#[method_id(markedTextRange)]
		unsafe fn markedTextRange(&self) -> Option<Id<UITextRange>> {
			let length = self.ivars().maked.borrow().length();
			// log::info!("markedTextRange 0-{}",length);
			Some(TextRange::new(0,length).to_ui())
		}
		#[method_id(markedTextStyle)]
		unsafe fn markedTextStyle(&self) -> Option<Id<NSDictionary<NSAttributedStringKey, AnyObject>>>{
			log::info!("markedTextStyle");
			None
		}
		#[method(setMarkedTextStyle:)]
		unsafe fn setMarkedTextStyle(&self,_: Option<&NSDictionary<NSAttributedStringKey, AnyObject>>){
			log::info!("setMarkedTextStyle");
		}
		#[method(setMarkedText:selectedRange:)]
		unsafe fn setMarkedText_selectedRange(&self,string: Option<&NSString>,_range: NSRange){
			// log::warn!("setMarkedText_selectedRange {string:?} {}-{}",_range.location,_range.length);
			if let Some(string) = string {
				let began = self.ivars().maked.borrow().length()<=0;
				*self.ivars().maked.borrow_mut()=NSMutableAttributedString::from_nsstring(string);
				let action = began.then_some(types::Action::Update).unwrap_or(types::Action::Begin);
				self.composition(types::Ime::new(action,Some(string.to_string().into())));
			}
		}
		#[method(unmarkText)]
		unsafe fn unmarkText(&self){
			let string = self.ivars().maked.borrow().mutableString();
			self.composition(types::Ime::new(types::Action::End,Some(string.to_string().into())));
			log::warn!("unmarkText {}",self.ivars().maked.borrow().mutableString());
			*self.ivars().maked.borrow_mut() = NSMutableAttributedString::new();
		}
		// Computing text ranges and text positions
		#[method_id(textRangeFromPosition:toPosition:)]
		unsafe fn textRangeFromPosition_toPosition(&self, start: &UITextPosition, end: &UITextPosition) -> Option<Id<UITextRange>>{
			// log::info!("textRangeFromPosition_toPosition");
			let start = TextPosition::from_ui(start).position();
			let end = TextPosition::from_ui(end).position();
			Some(TextRange::new(start,end).to_ui())
		}
		#[method_id(positionFromPosition:inDirection:offset:)]
		unsafe fn positionFromPosition_inDirection_offset(&self,_: &UITextPosition,_: UITextLayoutDirection,_: NSInteger) -> Option<Id<UITextPosition>>{
			log::info!("positionFromPosition_inDirection_offset");
			None
		}
		#[method_id(positionFromPosition:offset:)]
		unsafe fn positionFromPosition_offset(&self,start: &UITextPosition,shift: NSInteger) -> Option<Id<UITextPosition>>{
			let start = TextPosition::from_ui(start).position() as NSInteger;
			let end = start + shift;
			// log::info!("positionFromPosition_offset {} {}",start,shift);
			if end >= 0 {
				let end = end as NSUInteger;
				let length = self.ivars().maked.borrow().length();
				if end < length {
					Some(TextPosition::new(end as _).to_ui())
				}else{
					Some(TextPosition::new(length).to_ui())
				}
			}else{
				Some(TextPosition::new(0).to_ui())
			}
		}
		#[method_id(beginningOfDocument)]
		unsafe fn beginningOfDocument(&self) -> Id<UITextPosition>{
			// log::info!("beginningOfDocument");
			TextPosition::new(0).to_ui()
		}
		#[method_id(endOfDocument)]
		unsafe fn endOfDocument(&self) -> Id<UITextPosition>{
			// log::info!("endOfDocument");
			let length = self.ivars().maked.borrow().length();
			TextPosition::new(length).to_ui()
		}
		// Evaluating text positions
		#[method(comparePosition:toPosition:)]
		unsafe fn comparePosition_toPosition(&self,v1: &UITextPosition,v2: &UITextPosition) -> NSComparisonResult{
			// log::info!("comparePosition_toPosition");
			let v1 = TextPosition::from_ui(v1).position();
			let v2 = TextPosition::from_ui(v2).position();
			if v1 > v2 {
				NSComparisonResult::Descending
			}else if v1 < v2 {
				NSComparisonResult::Ascending
			}else {
				NSComparisonResult::Same
			}
		}
		#[method(offsetFromPosition:toPosition:)]
		unsafe fn offsetFromPosition_toPosition(&self,v1: &UITextPosition,v2: &UITextPosition,) -> NSInteger{
			let start = TextPosition::from_ui(v1).position();
			let end = TextPosition::from_ui(v2).position();
			// log::info!("offsetFromPosition_toPosition {} {}",start,end);
			(end - start) as _
		}
		// Determining layout and writing direction
		#[method_id(positionWithinRange:farthestInDirection:)]
		unsafe fn positionWithinRange_farthestInDirection(&self,_: &UITextRange,_: UITextLayoutDirection) -> Option<Id<UITextPosition>>{
			log::info!("positionWithinRange_farthestInDirection");
			None
		}
		#[method_id(characterRangeByExtendingPosition:inDirection:)]
		unsafe fn characterRangeByExtendingPosition_inDirection(&self,_: &UITextPosition,_: UITextLayoutDirection) -> Option<Id<UITextRange>>{
			log::info!("characterRangeByExtendingPosition_inDirection");
			None
		}
		#[method(baseWritingDirectionForPosition:inDirection:)]
		unsafe fn baseWritingDirectionForPosition_inDirection(&self,_: &UITextPosition,_: UITextStorageDirection) -> NSWritingDirection{
			log::info!("baseWritingDirectionForPosition_inDirection");
			NSWritingDirection::Natural
		}
		#[method(setBaseWritingDirection:forRange:)]
		unsafe fn setBaseWritingDirection_forRange(&self,_: NSWritingDirection,_: &UITextRange){
			log::info!("setBaseWritingDirection_forRange");
		}
		// Working with geometry and hit-testing
		#[method(firstRectForRange:)]
		unsafe fn firstRectForRange(&self, range: &UITextRange) -> CGRect{
			let range = TextRange::from_ui(range).to_range();
			// log::info!("firstRectForRange {}-{}",range.location,range.length);
			CGRect::new(CGPoint::new(range.location as _,0.),CGSize::new(range.length as _,1.))
		}
		#[method(caretRectForPosition:)]
		unsafe fn caretRectForPosition(&self, pos: &UITextPosition) -> CGRect {
			let pos = TextPosition::from_ui(pos).position();
			// log::info!("caretRectForPosition");
			CGRect::new(CGPoint::new(pos as _,0.),CGSize::new(1.,1.))
		}
		#[method_id(closestPositionToPoint:)]
		unsafe fn closestPositionToPoint(&self, _: CGPoint)-> Option<Id<UITextPosition>>{
			log::info!("closestPositionToPoint");
			None
		}
		#[method_id(selectionRectsForRange:)]
		unsafe fn selectionRectsForRange(&self, _: &UITextRange) -> Id<NSArray<UITextSelectionRect>> {
			// log::info!("selectionRectsForRange");
			NSArray::new()
		}
		#[method_id(closestPositionToPoint:withinRange:)]
		unsafe fn closestPositionToPoint_withinRange(&self,_: CGPoint,_: &UITextRange) -> Option<Id<UITextPosition>>{
			log::info!("closestPositionToPoint_withinRange");
			None
		}
		#[method_id(characterRangeAtPoint:)]
		unsafe fn characterRangeAtPoint(&self, _: CGPoint) -> Option<Id<UITextRange>> {
			log::info!("characterRangeAtPoint");
			None
		}
		// Tokenizing input text
		#[method_id(tokenizer)]
		unsafe fn tokenizer(&self) -> Id<ProtocolObject<dyn UITextInputTokenizer>>{
			// log::info!("tokenizer");
			let mut tokenizer = self.ivars().tokenizer.borrow_mut();
			if let Some(tokenizer) = tokenizer.as_ref() {
				ProtocolObject::from_retained(tokenizer.clone())
			}else{
				let mtm = MainThreadMarker::new().unwrap();
				let new = UITextInputStringTokenizer::initWithTextInput(mtm.alloc(),self);
				*tokenizer = Some(new.clone());
				ProtocolObject::from_retained(new)
			}
		}
	}
	unsafe impl XLoopView {
		#[method(draw)]
		unsafe fn draw(&self){ self.draw_() }
		#[method(canBecomeFirstResponder)]
		unsafe fn canBecomeFirstResponder(&self) -> bool { true }
		#[method(canResignFirstResponder)]
		unsafe fn canResignFirstResponder(&self) -> bool { true }

		#[method(touchesBegan:withEvent:)]
		fn touchesBegan(&self, touches: &NSSet<UITouch>, event: Option<&UIEvent>) {
			unsafe {
				if !self.isFirstResponder() { self.becomeFirstResponder() }else{ self.resignFirstResponder() };
			}
			self.touch(touches,event)
		}
		#[method(touchesMoved:withEvent:)]
		fn touchesMoved(&self, touches: &NSSet<UITouch>, event: Option<&UIEvent>) { self.touch(touches,event) }
		#[method(touchesEnded:withEvent:)]
		fn touchesEnded(&self, touches: &NSSet<UITouch>, event: Option<&UIEvent>) { self.touch(touches,event) }
		#[method(touchesCancelled:withEvent:)]
		fn touchesCancelled(&self, touches: &NSSet<UITouch>, event: Option<&UIEvent>) { self.touch(touches,event) }

		#[method(pressesBegan:withEvent:)]
		unsafe fn pressesBegan_withEvent(&self,presses: &NSSet<UIPress>,event: Option<&UIPressesEvent>){ self.press(presses,event) }
		#[method(pressesChanged:withEvent:)]
		unsafe fn pressesChanged_withEvent(&self,presses: &NSSet<UIPress>,event: Option<&UIPressesEvent>){ self.press(presses,event) }
		#[method(pressesEnded:withEvent:)]
		unsafe fn pressesEnded_withEvent(&self,presses: &NSSet<UIPress>,event: Option<&UIPressesEvent>){ self.press(presses,event) }
		#[method(pressesCancelled:withEvent:)]
		unsafe fn pressesCancelled_withEvent(&self,presses: &NSSet<UIPress>,event: Option<&UIPressesEvent>){ self.press(presses,event) }
	}
}

impl XLoopView {
	fn draw_(&self) {
		if let Some(window) = self.ivars().vars.window.borrow().clone() {
			let time = crate::CABase::CACurrentMediaTime();
			let nanos = time * 1_000_000_000.;
			let nanos = nanos as i64;
			let vars = &self.ivars().vars;
			if vars.refresh.get() {
				vars.refresh.set(false);
				vars.rep.on_req_draw(WinRef(&window, &self), nanos);
			}
		}
	}
	fn touch(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
		let vars = self.ivars();
		let mut caches = vars.vars.touches.borrow_mut();
		let caches = caches.as_mut();
		let mut ids = TouchId(caches);
		for touch in touches {
			let phase = touch.phase();
			match phase {
				UITouchPhase::Began | UITouchPhase::Moved => {
					let id = ids.insert(touch as _);
					if let Some(window) = vars.vars.window.borrow().clone() {
						vars.vars.rep.on_touch(WinRef(&window, &self), WinTouch(id, touch));
					}
				}
				UITouchPhase::Ended | UITouchPhase::Cancelled => {
					let id = ids.insert(touch as _);
					if let Some(window) = vars.vars.window.borrow().clone() {
						vars.vars.rep.on_touch(WinRef(&window, &self), WinTouch(id, touch));
					}
					ids.remove(touch as _);
				}
				_ => {
					println!("UITouchPhase {:?}", phase);
				}
			}
		}
	}
	fn press(&self, presses: &NSSet<UIPress>, _event: Option<&UIPressesEvent>) {
		let vars = self.ivars();
		for press in presses {
			if let Some(key) = unsafe { press.key(MainThreadMarker::new().unwrap()) } {
				if let Some(win) = vars.vars.window.borrow().as_ref() {
					vars.vars.rep.on_keyboard(WinRef(&win, &self), WinKeyboard(&key, press));
				}
			} else {
				// TODO other key
			}
		}
	}
	fn composition(&self, evt: types::Ime) {
		let vars = self.ivars();
		if let Some(win) = vars.vars.window.borrow().as_ref() {
			vars.vars.rep.on_ime(WinRef(&win, self), &evt);
		}
	}
}

pub struct TouchId<'a>(pub &'a mut Vec<Option<*const UITouch>>);
impl<'a> TouchId<'a> {
	pub fn insert(&mut self, id: *const UITouch) -> u8 {
		let mut idx = self.0.iter().enumerate().find(|(_, v)| **v == Some(id)).map(|v| v.0);
		if idx.is_none() {
			idx = self.0.iter_mut().enumerate().find(|(_, v)| v.is_none()).map(|(idx, v)| {
				*v = Some(id);
				idx
			})
		}
		if let Some(idx) = idx {
			idx as u8
		} else {
			let idx = self.0.len();
			self.0.push(Some(id));
			idx as u8
		}
	}
	pub fn remove(&mut self, id: *const UITouch) {
		self.0.iter_mut().find(|v| **v == Some(id)).map(|v| *v = None);
	}
}

declare_class! {
	pub struct TextPosition;
	unsafe impl ClassType for TextPosition {
		#[inherits(NSObject)]
		type Super = UITextPosition;
		type Mutability = mutability::MainThreadOnly;
		const NAME: &'static str = "XLoopTextPosition";
	}
	impl DeclaredClass for TextPosition {
		type Ivars = NSUInteger;
	}
}
impl TextPosition {
	pub fn new(position: NSUInteger) -> Id<Self> {
		let mtm = MainThreadMarker::new().unwrap();
		unsafe { msg_send_id![super(mtm.alloc().set_ivars(position)), init] }
	}
	pub fn position(&self) -> NSUInteger {
		*self.ivars()
	}
	pub fn to_ui(&self) -> Id<UITextPosition> {
		unsafe { Id::retain(self as *const _ as *mut _) }.unwrap()
	}
	pub fn from_ui(v: &UITextPosition) -> &Self {
		unsafe { &*(v as *const _ as *const Self) }
	}
}
declare_class! {
	pub struct TextRange;
	unsafe impl ClassType for TextRange {
		#[inherits(NSObject)]
		type Super = UITextRange;
		type Mutability = mutability::MainThreadOnly;
		const NAME: &'static str = "XLoopTextRange";
	}
	impl DeclaredClass for TextRange {
		type Ivars = (Id<TextPosition>,Id<TextPosition>);
	}
	unsafe impl TextRange {
		#[method(isEmpty)]
		unsafe fn isEmpty(&self) -> bool{
			let vars = self.ivars();
			vars.1.position() > vars.0.position()
		}
		#[method_id(start)]
		unsafe fn start(&self) -> Id<UITextPosition>{
			self.ivars().0.to_ui()
		}
		#[method_id(end)]
		unsafe fn end(&self) -> Id<UITextPosition>{
			self.ivars().1.to_ui()
		}
	}
}
impl TextRange {
	pub fn new(start: NSUInteger, end: NSUInteger) -> Id<Self> {
		let mtm = MainThreadMarker::new().unwrap();
		unsafe {
			msg_send_id![
				super(mtm.alloc().set_ivars((TextPosition::new(start), TextPosition::new(end)))),
				init
			]
		}
	}
	pub fn clone(&self) -> Id<TextRange> {
		Self::new(self.ivars().0.position(), self.ivars().1.position())
	}
	pub fn length(&self) -> NSUInteger {
		// log::info!("length {} {}", self.ivars().1.position(), self.ivars().0.position());
		self.ivars().1.position() - self.ivars().0.position()
	}
	pub fn to_range(&self) -> NSRange {
		NSRange::new(self.ivars().0.position(), self.length())
	}
	pub fn to_ui(&self) -> Id<UITextRange> {
		unsafe { Id::retain(self as *const _ as *mut _) }.unwrap()
	}
	pub fn from_ui(v: &UITextRange) -> &Self {
		unsafe { &*(v as *const _ as *const Self) }
	}
}
